#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@NAME          : ftp_util.py
@TIME          : 2025/02/06 22:44:49
@AUTHOR        : chenlip
@VERSION       : 0.0.1
@DESCRIPTION   : FTP工具类
    使用ftplib模块实现FTP文件上传、下载、删除等操作
    1- 连接FTP服务器
    2- 登录FTP服务器和退出FTP服务器
    3- 检查FTP服务器是否支持list命令/是否支持UTF-8编码/是否支持MLSD命令/是否支持MDTM命令
    4- 获取list命令的输出格式，根据输出格式解析文件信息（包含文件名称、文件大小，创建时间、修改时间）
    5- 上传文件到FTP服务器，支持异常处理，如果上传失败，可以重试3次
    6- 从FTP服务器下载文件，支持异常处理，如果下载失败，可以重试3次
    7- 删除FTP服务器上的文件

'''
# --------------------------引用--------------------------
import os, sys, json
from ftplib import FTP, error_perm
import socket
from datetime import datetime
import re
from typing import Dict, List, Optional, Tuple
from app.common.logutil import log as logger
import time
# --------------------------引用--------------------------

class FTPUtil:
    def __init__(self, host: str, username: str = None, password: str = None, port: int = 21, timeout: int = 30):
        """
        初始化FTP工具类
        
        Args:
            host (str): FTP服务器地址，例如: 'ftp.example.com' 或 '192.168.1.100'
            username (str, optional): FTP用户名. 默认为 None
            password (str, optional): FTP密码. 默认为 None
            port (int, optional): FTP服务器端口号. 默认为 21
            timeout (int, optional): 连接超时时间(秒). 默认为 30
        
        Example:
            >>> ftp = FTPUtil('ftp.example.com', username='user', password='pass', port=21, timeout=30)
        """
        self.host = host
        self.username = username
        self.password = password
        self.port = port
        self.timeout = timeout
        self.ftp = None
        self.logger = logger
        # 初始化时连接FTP服务器，检查连接状态和服务器特性
        self.connectstatus = self.connect()

        self.fetures = self.check_features() if self.connectstatus else None

    def connect(self) -> bool:
        """
        连接到FTP服务器并自动登录(如果提供了用户名和密码)，失败时最多重试3次，每次间隔5秒
        
        Returns:
            bool: 连接和登录成功返回True，失败返回False
        
        Example:
            >>> ftp = FTPUtil('ftp.example.com', username='user', password='pass')
            >>> if ftp.connect():
            ...     print("连接和登录成功")
            ... else:
            ...     print("连接或登录失败")
        """
        max_retries = 3
        
        for attempt in range(max_retries):
            try:
                self.ftp = FTP()
                self.ftp.connect(self.host, self.port, self.timeout)
                
                # 如果提供了用户名和密码，尝试自动登录
                if self.username and self.password:
                    return self.login(self.username, self.password)
                return True
                
            except (socket.error, socket.gaierror) as e:
                self.logger.error(f"连接FTP服务器失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
                if attempt < max_retries - 1:
                    time.sleep(5)  # 等待5秒后重试
                else:
                    return False

    def login(self, username: str, password: str) -> bool:
        """
        登录FTP服务器
        
        Args:
            username (str): FTP用户名
            password (str): FTP密码
        
        Returns:
            bool: 登录成功返回True，失败返回False
            
        Example:
            >>> ftp = FTPUtil('ftp.example.com')
            >>> if ftp.connect() and ftp.login('user', 'password'):
            ...     print("登录成功")
        """
        try:
            self.ftp.login(username, password)
            return True
        except error_perm as e:
            self.logger.error(f"登录FTP服务器失败: {str(e)}")
            return False

    def quit(self):
        """退出FTP连接"""
        if self.ftp:
            try:
                self.ftp.quit()
            except:
                self.ftp.close()

    def check_features(self) -> Dict[str, bool]:
        """
        检查FTP服务器支持的特性
        
        Returns:
            Dict[str, bool]: 包含服务器特性支持情况的字典
                - 'LIST': 是否支持列表命令
                - 'UTF8': 是否支持UTF-8编码
                - 'MLSD': 是否支持机器可读的目录列表
                - 'MDTM': 是否支持文件修改时间获取
        
        Example:
            >>> features = ftp.check_features()
            >>> if features['UTF8']:
            ...     print("服务器支持UTF-8编码")
        """
        features = {
            'LIST': False,
            'UTF8': False,
            'MLSD': False,
            'MDTM': False
        }
        
        try:
            feat_list = self.ftp.sendcmd('FEAT').split('\n')
            features['LIST'] = True  # LIST是基本命令，默认支持
            features['UTF8'] = any(' UTF8' in line for line in feat_list)
            features['MLSD'] = any(' MLSD' in line for line in feat_list)
            features['MDTM'] = any(' MDTM' in line for line in feat_list)
        except:
            pass

        print(f'ftp服务器支持相关命令情况: {features}')
        
        return features
    
    def get_ftp_list_file(self, remote_path: str = '/', topcount: int = 10) -> List[Dict]:
        """
        获取FTP服务器上的文件列表，按修改时间倒序排列

        Args:
            remote_path (str, optional): FTP服务器上的目标路径. 默认为根目录'/'
            topcount (int, optional): 返回的文件数量，0表示返回所有文件. 默认为10
        
        Returns:
            List[Dict]: 包含FTP服务器上文件信息的列表，每个元素是一个字典，包含以下字段：
                - name: 文件名
                - size: 文件大小(字节)
                - type: 文件类型('file'或'dir')
                - modify_time: 修改时间
                如果获取失败返回空列表
        
        Example:
            >>> files = ftp.get_ftp_list_file('/', 5)  # 获取最新的5个文件
            >>> for file in files:
            ...     print(f"文件名: {file['name']}, 修改时间: {file['modify_time']}")
        """
        files = []
        
        try:
            if self.fetures['MLSD']:
                self.ftp.sendcmd('TYPE I')
                for name, facts in self.ftp.mlsd(remote_path):
                    if name not in ['.', '..']:
                        files.append({
                            'name': name,
                            'size': int(facts.get('size', 0)),
                            'type': facts.get('type', 'file'),
                            'modify_time': facts.get('modify', 'N/A')
                        })
            else:
                lines = []
                self.ftp.dir(remote_path, lines.append)
                for line in lines:
                    info = self.parse_list_output(line)
                    if info:
                        files.append(info)

            # 按修改时间倒序排序
            files.sort(key=lambda x: x['modify_time'], reverse=True)
            
            # 如果指定了文件数量且不为0，则只返回指定数量的文件
            if topcount > 0:
                files = files[:topcount]
                
        except Exception as e:
            self.logger.error(f"获取FTP文件列表失败: {str(e)}")
        
        return files
    
    def get_ftp_file_info(self, remote_path: str) -> Optional[Dict]:
        """
        获取FTP服务器上指定文件的信息
        
        Args:
            remote_path (str): FTP服务器上的文件路径
        
        Returns:
            Optional[Dict]: 包含文件信息的字典，包含以下字段：
                - name: 文件名
                - size: 文件大小(字节)
                - type: 文件类型('file'或'dir')
                - modify_time: 修改时间
                如果获取失败返回None
        
        Example:
            >>> info = ftp.get_ftp_file_info('/remote/file.txt')
            >>> if info:
            ...     print(f"文件名: {info['name']}, 大小: {info['size']}")
        """
        try:
            dir_path = os.path.dirname(remote_path)
            file_name = os.path.basename(remote_path)
            
            if self.fetures['MLSD']:
                self.ftp.sendcmd('TYPE I')  # 设置传输模式为二进制
                for name, facts in self.ftp.mlsd(dir_path or '.'):
                    if name == file_name:
                        return {
                            'name': name,
                            'size': int(facts.get('size', 0)),
                            'type': 'dir' if facts.get('type', '') == 'dir' else 'file',
                            'modify_time': facts.get('modify', 'N/A')
                        }
            else:
                # 获取目录下的文件列表
                lines = []
                self.ftp.dir(dir_path, lines.append)
                # 查找匹配的文件
                for line in lines:
                    info = self.parse_list_output(line)
                    if info and info['name'] == file_name:
                        return info
        except Exception as e:
            self.logger.error(f"获取FTP文件信息失败: {str(e)}")

    def parse_list_output(self, line: str) -> Optional[Dict]:
        """
        解析FTP LIST命令输出的单行信息
        
        Args:
            line (str): LIST命令输出的单行文本
                Unix格式示例: "-rw-r--r-- 1 user group 2048 Jan 1 12:00 file.txt"
                DOS格式示例: "01-01-20 12:00PM 2048 file.txt"
        
        Returns:
            Optional[Dict]: 解析后的文件信息字典，包含以下字段：
                - name: 文件名
                - size: 文件大小(字节)
                - type: 文件类型('file'或'dir')
                - modify_time: 修改时间
                如果解析失败返回None
        
        Example:
            >>> info = ftp.parse_list_output("-rw-r--r-- 1 user group 2048 Jan 1 12:00 file.txt")
            >>> if info:
            ...     print(f"文件名: {info['name']}, 大小: {info['size']}")
        """
        unix_pattern = r'^([\-ld])([rwxt\-]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w+\s+\d+\s+[\d:]+)\s+(.+)$'
        dos_pattern = r'^(\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[AP]M)\s+(<DIR>|\d+)\s+(.+)$'
        
        try:
            # 尝试Unix格式解析
            unix_match = re.match(unix_pattern, line)
            if unix_match:
                unix_time = FTPUtil.parse_unix_time(unix_match.group(7))
                return {
                    'name': unix_match.group(8),
                    'size': int(unix_match.group(6)),
                    'type': 'dir' if unix_match.group(1) == 'd' else 'file',
                    'modify_time': unix_time.strftime('%Y-%m-%d %H:%M:%S')
                }

            # 尝试DOS格式解析
            dos_match = re.match(dos_pattern, line)
            if dos_match:
                # Convert DOS date format (MM-DD-YY HH:MM[AP]M) to YYYY-MM-DD HH:MM:SS
                dos_time = datetime.strptime(dos_match.group(1), '%m-%d-%y %I:%M%p')
                return {
                    'name': dos_match.group(3),
                    'size': 0 if dos_match.group(2) == '<DIR>' else int(dos_match.group(2)),
                    'type': 'dir' if dos_match.group(2) == '<DIR>' else 'file',
                    'modify_time': dos_time.strftime('%Y-%m-%d %H:%M:%S')
                }
        except:
            pass

        return None

    def upload_file(self, local_path: str, remote_path: str, max_retries: int = 3) -> bool:
        """
        上传文件到FTP服务器
        
        Args:
            local_path (str): 本地文件路径，例如: 'D:/files/document.pdf'
            remote_path (str): FTP服务器上的目标路径，例如: '/upload/document.pdf'
            max_retries (int, optional): 上传失败时的最大重试次数. 默认为 3
        
        Returns:
            bool: 上传成功返回True，失败返回False
        
        Example:
            >>> success = ftp.upload_file(
            ...     local_path='local/file.txt',
            ...     remote_path='/remote/file.txt',
            ...     max_retries=3
            ... )
            >>> print("上传成功" if success else "上传失败")
        """
        if not os.path.exists(local_path):
            self.logger.error(f"本地文件不存在: {local_path}")
            return False

        for attempt in range(max_retries):
            try:
                with open(local_path, 'rb') as f:
                    self.ftp.storbinary(f'STOR {remote_path}', f)
                return True
            except Exception as e:
                self.logger.error(f"上传文件失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
                if attempt == max_retries - 1:
                    return False
                continue

    def download_file(self, remote_path: str, local_path: str, max_retries: int = 3) -> bool:
        """
        从FTP服务器下载文件
        
        Args:
            remote_path (str): FTP服务器上的文件路径，例如: '/download/document.pdf'
            local_path (str): 本地保存路径，例如: 'D:/files'
            max_retries (int, optional): 下载失败时的最大重试次数. 默认为 3
        
        Returns:
            bool: 下载成功返回True，失败返回False
        
        Example:
            >>> success = ftp.download_file(
            ...     remote_path='/remote/file.txt',
            ...     local_path='local',
            ...     max_retries=3
            ... )
            >>> print("下载成功" if success else "下载失败")
        """
        for attempt in range(max_retries):
            try:
                os.makedirs(local_path, exist_ok=True)
                local_file_path = os.path.join(local_path, os.path.basename(remote_path))
                with open(local_file_path, 'wb') as f:
                    self.ftp.retrbinary(f'RETR {remote_path}', f.write)

                return True
            except Exception as e:
                self.logger.error(f"下载文件失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
                if attempt == max_retries - 1:
                    return False
                continue

    def delete_file(self, remote_path: str) -> bool:
        """
        删除FTP服务器上的文件
        
        Args:
            remote_path (str): 要删除的文件在FTP服务器上的路径
                例如: '/upload/document.pdf'
        
        Returns:
            bool: 删除成功返回True，失败返回False
        
        Example:
            >>> if ftp.delete_file('/remote/file.txt'):
            ...     print("文件删除成功")
            ... else:
            ...     print("文件删除失败")
        """
        try:
            self.ftp.delete(remote_path)
            return True
        except Exception as e:
            self.logger.error(f"删除文件失败: {str(e)}")
            return False

    @staticmethod
    def parse_unix_time(time_str: str) -> datetime:
        """更智能地解析Unix格式的时间"""
        current_time = datetime.now()
        parsed_time = datetime.strptime(time_str, '%b %d %H:%M')
        
        # 设置年份
        parsed_time = parsed_time.replace(year=current_time.year)
        
        # 如果解析后的时间比当前时间晚，说明可能是去年的文件
        if parsed_time > current_time:
            parsed_time = parsed_time.replace(year=current_time.year - 1)
        
        return parsed_time


